package com.hulk.dryad.manage.framework.knife4j;

import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import com.hulk.dryad.common.constant.CommonConstants;
import com.hulk.dryad.common.constant.SecurityConstants;
import com.hulk.dryad.manage.framework.knife4j.Knife4jProperties.Authorization;
import com.hulk.dryad.manage.framework.knife4j.Knife4jProperties.GrantTypeEnum;
import com.hulk.dryad.manage.framework.knife4j.Knife4jProperties.Oauth2;
import com.hulk.dryad.manage.security.component.PermitAllSecurityUrlProperties;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import springfox.documentation.builders.*;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.ApiKeyVehicle;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * swagger配置
 *
 * @author hulk
 */
@Configuration
@ConditionalOnClass(Docket.class)
@EnableConfigurationProperties({Knife4jProperties.class, PermitAllSecurityUrlProperties.class})
@ConditionalOnProperty(name = "enabled", prefix = Knife4jProperties.PREFIX, havingValue = "true", matchIfMissing = true)
public class Knife4jConfiguration {


    @Bean
    public Docket docket(Environment environment,
		  Knife4jProperties properties,
		  PermitAllSecurityUrlProperties permitAllSecurityUrlProperties,
		  ObjectProvider<List<Knnife4jCustomizer>> swaggerCustomizersProvider) {
    //1. 组名为应用名
    String appName = environment.getProperty(CommonConstants.SPRING_APP_NAME_KEY);
    Docket docket =
        new Docket(DocumentationType.SWAGGER_2)
            .useDefaultResponseMessages(false)
            .globalRequestParameters(globalHeaders(properties))
            .apiInfo(apiInfo(appName, properties))
            .select()
            .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
            .paths(PathSelectors.any())
            .build();
    //2. 如果开启 apiKey 认证
    if (properties.getAuthorization().getEnabled()) {
      Knife4jProperties.Authorization authorization = properties.getAuthorization();
      authorization.getPathPatterns().addAll(permitAllSecurityUrlProperties.getPermitAll());
      docket.securitySchemes(Collections.singletonList(apiKey(authorization)));
      docket.securityContexts(Collections.singletonList(apiKeySecurityContext(authorization)));
    }
    //3. 如果开启 oauth2 认证
    if (properties.getOauth2().getEnabled()) {
      Oauth2 oauth2 = properties.getOauth2();
      oauth2.getPathPatterns().addAll(permitAllSecurityUrlProperties.getPermitAll());
      docket.securitySchemes(Collections.singletonList(oauth2(oauth2)));
      docket.securityContexts(Collections.singletonList(oauth2SecurityContext(oauth2)));
    }
	//4. 自定义 customizer 配置
	swaggerCustomizersProvider.ifAvailable(customizers -> customizers.forEach(customizer -> customizer.customize(docket)));
	return docket;
  }

  /**
   * 配置基于 ApiKey 的鉴权对象
   *
   * @return {ApiKey}
   */
  private ApiKey apiKey(Authorization authorization) {
    return new ApiKey(
        authorization.getName(), authorization.getKeyName(), ApiKeyVehicle.HEADER.getValue());
  }

  /**
   * 配置默认的全局鉴权策略的开关，以及通过正则表达式进行匹配；默认 /** 匹配所有URL 其中 securityReferences 为配置启用的鉴权策略
   *
   * @return {SecurityContext}
   */
  private SecurityContext apiKeySecurityContext(Authorization authorization) {

    final List<String> pathPatterns = new ArrayList<>(authorization.getPathPatterns());
    if (pathPatterns.isEmpty()) {
      pathPatterns.add("/**");
    }
    final AntPathMatcher matcher = new AntPathMatcher();
    return SecurityContext.builder()
        .securityReferences(apiKeyAuth(authorization))
        .operationSelector(
            (context) -> {
              String mappingPattern = context.requestMappingPattern();
              boolean b =
                  pathPatterns.stream()
                      .noneMatch(patterns -> matcher.match(patterns, mappingPattern));
              return b;
            })
        .build();
  }

  /**
   * 配置默认的全局鉴权策略；其中返回的 SecurityReference 中，reference 即为ApiKey对象里面的name，保持一致才能开启全局鉴权
   *
   * @return {List<SecurityReference>}
   */
  private List<SecurityReference> apiKeyAuth(Authorization authorization) {
    AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
    authorizationScopes[0] = new AuthorizationScope("global", "accessEverything");
    return Collections.singletonList(
        SecurityReference.builder()
            .reference(authorization.getName())
            .scopes(authorizationScopes)
            .build());
  }

  private OAuth oauth2(Oauth2 oauth2) {

    List<GrantType> singletonList = Lists.newArrayList();
    for (GrantTypeEnum grantTypeEnum : oauth2.getGrantTypes()) {
      GrantType grantType = null;
      String tokenUrl =
          StrUtil.isBlank(oauth2.getTokenUrl())
              ? SecurityConstants.OAUTH_TOKEN_URL
              : oauth2.getTokenUrl();
      // 授权码模式
      if (GrantTypeEnum.AUTHORIZATION_CODE == grantTypeEnum) {
        TokenRequestEndpoint tokenRequestEndpoint =
            new TokenRequestEndpointBuilder()
                .url(oauth2.getAuthorizeUrl())
                .clientIdName(oauth2.getClientIdName())
                .clientSecretName(oauth2.getClientSecretName())
                .build();
        TokenEndpoint tokenEndpoint =
            new TokenEndpointBuilder().url(tokenUrl).tokenName(oauth2.getTokenName()).build();
        grantType = new AuthorizationCodeGrant(tokenRequestEndpoint, tokenEndpoint);
      } else if (GrantTypeEnum.CLIENT_CREDENTIALS == grantTypeEnum) {
        grantType = new ClientCredentialsGrant(tokenUrl);
      } else if (GrantTypeEnum.IMPLICIT == grantTypeEnum) {
        LoginEndpoint loginEndpoint = new LoginEndpoint(oauth2.getAuthorizeUrl());
        grantType = new ImplicitGrant(loginEndpoint, oauth2.getTokenName());
      } else if (GrantTypeEnum.PASSWORD == grantTypeEnum) {
        grantType = new ResourceOwnerPasswordCredentialsGrant(tokenUrl);
      }
      singletonList.add(grantType);
    }
    return new OAuthBuilder().name(oauth2.getName()).grantTypes(singletonList).build();
  }

  private SecurityContext oauth2SecurityContext(Oauth2 oauth2) {
    List<AuthorizationScope> scopes = new ArrayList<>();
    List<Knife4jProperties.CustomerAuthorizationScope> oauth2Scopes = oauth2.getScopes();
    for (Knife4jProperties.CustomerAuthorizationScope oauth2Scope : oauth2Scopes) {
      scopes.add(new AuthorizationScope(oauth2Scope.getScope(), oauth2Scope.getDescription()));
    }
    SecurityReference securityReference =
        new SecurityReference(oauth2.getName(), scopes.toArray(new AuthorizationScope[0]));
    final List<String> pathPatterns = new ArrayList<>(oauth2.getPathPatterns());
    if (pathPatterns.isEmpty()) {
      pathPatterns.add("/**");
    }
    final AntPathMatcher matcher = new AntPathMatcher();
    return SecurityContext.builder()
        .securityReferences(Collections.singletonList(securityReference))
        .operationSelector(
            (context) -> {
              String mappingPattern = context.requestMappingPattern();
              boolean b =
                  pathPatterns.stream()
                      .noneMatch(patterns -> matcher.match(patterns, mappingPattern));
              return b;
            })
        .build();
  }

  private ApiInfo apiInfo(@Nullable String appName, Knife4jProperties properties) {
    String defaultName = (appName == null ? "" : appName) + "服务";
    String title = Optional.ofNullable(properties.getTitle()).orElse(defaultName);
    String description = Optional.ofNullable(properties.getDescription()).orElse(defaultName);
    return new ApiInfoBuilder()
        .title(title)
        .description(description)
        .version(properties.getVersion())
        .contact(
            new Contact(
                properties.getContactUser(),
                properties.getContactUrl(),
                properties.getContactEmail()))
        .build();
  }

  private List<RequestParameter> globalHeaders(Knife4jProperties properties) {
    return properties.getHeaders().stream()
        .map(
            header ->
                new RequestParameterBuilder()
                    .in(ParameterType.HEADER)
                    .name(header.getName())
                    .description(header.getDescription())
                    .required(header.isRequired())
                    .build())
        .collect(Collectors.toList());
  }
}
